package city.spring.controller.base;

import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

/**
 * 自定义下载控制器。只能访问服务器本地的文件信息。
 * 支持断点下载，支持默认中文文件名
 *
 * @author HouKunLin
 * @date 2020/1/7 0007 15:56
 */
@Getter
@Setter
@Controller
@RequestMapping
public class CustomDownloadController extends ResourceHttpRequestHandler {
    private static final Logger logger = LoggerFactory.getLogger(CustomDownloadController.class);
    /**
     * 默认下载文件名
     */
    private static final String DEFAULT_DOWNLOAD_FILENAME = "download-unknown-filename.unknown";
    private final boolean isDebug = logger.isDebugEnabled();
    private PathResourceResolver pathResourceResolver = new PathResourceResolver();
    /**
     * 强制下载参数
     */
    private String compulsoryDownloadParameter = "download";
    /**
     * 自动强制下载值
     */
    private String autoCompulsoryDownloadValue = "auto";
    /**
     * 自动强制下载长度，当启用自动强制下载功能（参数：?download=auto）时，如果下载资源长度大于该值则启用强制下载
     * 1024 * 1024 * 5 = 5MB 大小
     */
    private long autoCompulsoryDownloadLength = 1024 * 1024 * 5;

    public CustomDownloadController(ResourceProperties resourceProperties) {
        // resourceProperties.getStaticLocations() 的地址列表包含：
        // class path resource [META-INF/resources/]
        // class path resource [resources/]
        // class path resource [static/]
        // class path resource [public/]
        List<String> list = new ArrayList<>(Arrays.asList(resourceProperties.getStaticLocations()));
        // ServletContext resource [/]
        list.add("/");
        super.setLocationValues(list);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        super.afterPropertiesSet();
        List<Resource> locations = getLocations();
        // 把系统文件的 D:/tmp/ 路径加入到可下载的路径列表中
        locations.add(new FileSystemResource("C:/pension-system/upload/"));
        locations.add(new FileSystemResource("/var/pension-system/upload/"));
    }

    @GetMapping("/files/**")
    public void download(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response) throws ServletException, IOException {
        handleRequest(request, response, (request1) -> {
            String uri = request1.getRequestURI();
            String path = uri.substring(request1.getContextPath().length() + "/files/".length());
            return pathResourceResolver.resolveResource(request1, path, getLocations(), null);
        });
    }

    @RequestMapping("/download")
    @Override
    public void handleRequest(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response) throws ServletException, IOException {
        handleRequest(request, response, this::getResource);
    }

    public void handleRequest(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, Function<HttpServletRequest, Resource> getResource) throws ServletException, IOException {
        // For very general mappings (e.g. "/") we need to check 404 first
        Resource resource = getResource.apply(request);
        if (resource == null) {
            logger.debug("Resource not found");
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
            response.setHeader("Allow", getAllowHeader());
            return;
        }

        // Supported methods and required session
        checkRequest(request);

        // Header phase
        if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
            logger.trace("Resource not modified");
            return;
        }

        // Apply cache settings, if any
        prepareResponse(response);

        // Check the media type for the resource
        MediaType mediaType = getMediaType(request, resource);

        // Content phase
        if (METHOD_HEAD.equals(request.getMethod())) {
            setHeaders(response, resource, mediaType);
            return;
        }

        ResourceHttpMessageConverter resourceHttpMessageConverter = getResourceHttpMessageConverter();
        ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter = getResourceRegionHttpMessageConverter();

        ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);

        // 判断是否需要设置强制下载
        setCompulsoryDownload(outputMessage, resource, isCompulsoryDownload(request, resource));

        if (request.getHeader(HttpHeaders.RANGE) == null) {
            Assert.state(resourceHttpMessageConverter != null, "Not initialized");
            setHeaders(response, resource, mediaType);
            logger.debug("直接下载文件：{}", resource.getFilename());
            resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
        } else {
            Assert.state(resourceRegionHttpMessageConverter != null, "Not initialized");
            response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
            ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request);
            try {
                List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
                logger.debug("断点续传：{} range: {}", resource.getFilename(), httpRanges);
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                resourceRegionHttpMessageConverter.write(
                        HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage);
            } catch (IllegalArgumentException ex) {
                response.setHeader("Content-Range", "bytes */" + resource.contentLength());
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            }
        }
    }

    /**
     * 获取需要下载的资源文件
     *
     * @param request
     * @return
     * @throws IOException
     */
    @SuppressWarnings("all")
    @Override
    protected Resource getResource(HttpServletRequest request) {
        // super.getResource(request); 默认的方法是通过URI来获取文件资源的，
        // 因为自定义下载路径，URI是固定的，需要从请求参数中获取文件资源信息
        String filename = request.getParameter("filename");
        if (filename == null) {
            return null;
        }

        return pathResourceResolver.resolveResource(request, filename, getLocations(), null);
    }

    /**
     * 判断是否启用强制下载（通过参数控制）。
     * 如果不启用强制下载，在遇到图片、文本文件等内容会直接在浏览器展示
     *
     * @param request 请求
     * @return boolean
     */
    private boolean isCompulsoryDownload(HttpServletRequest request, Resource resource) throws IOException {
        // 强制下载参数
        String compulsoryDownloadValue = obtainCompulsoryDownload(request);
        // 通过参数来决定是否启用自动强制下载判断策略
        boolean autoCompulsoryDownloadParameter = autoCompulsoryDownloadValue.equals(compulsoryDownloadValue);

        // 自动强制下载判断策略执行结果
        boolean autoCompulsoryDownload = autoCompulsoryDownloadParameter && resource.contentLength() > autoCompulsoryDownloadLength;

        // 浏览器启动直接强制下载
        boolean compulsoryDownload = !autoCompulsoryDownloadParameter && compulsoryDownloadValue != null;
        return autoCompulsoryDownload || compulsoryDownload;
    }

    /**
     * 获得强制下载参数值
     *
     * @param request 请求
     * @return 强制下载参数值
     */
    private String obtainCompulsoryDownload(HttpServletRequest request) {
        return request.getParameter(compulsoryDownloadParameter);
    }

    /**
     * 设置强制下载参数
     *
     * @param outputMessage        输出信息
     * @param resource             下载的资源信息
     * @param isCompulsoryDownload 是否启用强制下载，该参数可以通过调用 this#isCompulsoryDownload(HttpServletRequest, Resource) 来获得
     */
    private void setCompulsoryDownload(ServletServerHttpResponse outputMessage, Resource resource, boolean isCompulsoryDownload) {
        HttpHeaders headers = outputMessage.getHeaders();
        // 默认的参数值，表示它可以显示在网页内，或作为网页，如果浏览器无法在页面内显示则弹出文件保存框
        String type = "inline";
        if (isCompulsoryDownload) {
            // 启用强制下载
            // 设置内容类型：未知的应用程序文件。使浏览器无法识别文件类型直接启用下载功能
            // 设置这个参数可以使浏览器使用完整的 filename 默认文件名，如果不设置可能出现以下情况：
            // 下载 txt text conf def list log in 类型文件时，默认 MediaType 都是 text/plain ，
            // 此时 filename 为 download.log 但是保存的默认文件为 download.txt 的问题
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            // 开启了强制下载功能，则告诉浏览器直接下载文件，不尝试在网页内展示内容
            type = "attachment";
        }
        String filename = resource.getFilename();
        filename = filename != null ? filename : DEFAULT_DOWNLOAD_FILENAME;
        // 告诉浏览器以附件的形式下载（直接启用下载功能），同时设置保持的文件名
        ContentDisposition disposition = ContentDisposition.builder(type)
                // 其中包含该字段的内容引用的 HTML 字段的名称。
                // .name(filename)
                // 是一个包含传输文件的原始名称的字符串。
                // 传入参数二 Charsets.UTF_8 可以支持中文文件名
                .filename(filename, StandardCharsets.UTF_8)
                .build();
        headers.setContentDisposition(disposition);
    }
}
